I was reading "Understanding ES6" by Nicholas Zakas and something caught my attention.
In a section on let/const, there was this snippet:
if (condition) {
console.log(typeof value); // ReferenceError!
let value = "blue";
}
I almost spilled my coffee.
Is typeof
operator no longer the absolutely and the only safe way to check for anything, whether it's undeclared values or something as crazy as IE's host objects with their "unknown" type and which blow up on [[Get]]?
I ran it in Chrome and FF just to confirm — both errored out.
Opened spec, looking at typeof
operator — behavior is more or less the same as in ES5. So what's the trick? How is it possible?
After some investigation, here's what I found.
First of all, 13.2.1 has an (informal?) note:
let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.
Notice "may not be accessed" (emphasis mine). It sort of hints at this behavior but this is informal and doesn't explain much.
Ok, let's take a look at what exactly is happening:
if (1) { typeof x; let x; }
1) When the block is evaluated
13.1.11 — BlockDeclarationInstantiation('typeof x; let x;')
For each element d in declarations do
...
Let status be the result of calling env’s CreateMutableBinding concrete method passing dn and false as the arguments.
↓
8.1.1.1.2 - CreateMutableBinding(x)
Create a mutable binding in envRec for N and record that it is uninitialized.
let x
creates a mutable lexical x
binding.
2) Execution of typeof x
12.5.6 — typeof x
↓
6.2.3.1 — GetValue(x)
↓
8.1.1 — GetBindingValue(x)
If the binding exists but is uninitialized a ReferenceError is thrown, regardless of the value of S.
(vs. in ES5)
return the value currently bound to N in envRec.
So (declared but) uninitialized bindings now throw ReferenceError's in ES6. But what happens with var's? Why don't they throw in similar cases? Doesn't var
also declare uninitialized binding?
if (1) { typeof x; var x; }
Turns out the answer is in 13.2.2 — VariableStatement:
Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created.
So the reason typeof x
doesn't throw for var x
but throws for let x
is because var
not only creates but also initializes binding to undefined
, whereas let
merely declares it.
Curiously, in ES5 var declarations create mutable bindings with undefined
values too. But the difference seems to be that in ES5 you couldn't create uninitialized mutable bindings (both variable and function declarations would always set values). In ES5, only CreateImmutableBinding operation could create uninitialized binding, and as far as I can see, there was no way to perform that in user code. CreateImmutableBinding was only used in 2 places — to create arguments
binding (in strict code) and to make NFE's function identifier available ("inject") to the scope of the function itself.
So there's a new typeof
— a long-standing rule that is no longer true in ES6